/* Tests producer/consumer communication with different numbers of threads.
 * Automatic checks only catch severe problems like crashes.
 */

#include <stdio.h>
#include "tests/threads/tests.h"
#include "threads/malloc.h"
#include "threads/synch.h"
#include "threads/thread.h"
#include "devices/timer.h"

void producer_consumer(unsigned int num_producer, unsigned int num_consumer);
void producer(void *aux);
void consumer(void *aux);

# define SZ_BUFF	2	/* reader buff size */

static char string[] = "Hello World";/* Producers put this into buffer */
static int len;
static char buf[SZ_BUFF];            /* the shared buffer */
static struct lock mutex,            /* for mutual exclusion b/w consumer/producer */ 
                      pl;            /* lock for serializable access to string[]   */
static struct condition full, empty; /* condition variable for shared buffer state */
static int i, k;                     /* k: current number of items in shared buf   */
static int in,                       /* in: index of producer to write in buf      */
           out;                      /* out: index of consumer which it reads,print*/

/* numC: num of consumers, if no threads -1, we wrote before the mail discussion
 * about consumer/producer behviour of blocking when their are no counterparts.
 * This has been done to prevent infinite wait due to first three tests.
 *
 * numP: num of proucers
 */
static int numC, numP;

/* Test thread serialization lock to ensure
 * that only one test case run at a time
 */
static struct lock output_lock;

void test_producer_consumer(void)
{
	lock_init(&output_lock);

   	producer_consumer(0, 0);
    producer_consumer(1, 0);
    producer_consumer(0, 1);
    producer_consumer(1, 1);
    producer_consumer(3, 1);
    producer_consumer(1, 3);
    producer_consumer(4, 4);
    producer_consumer(7, 2);
    producer_consumer(2, 7);
    producer_consumer(6, 6);
    pass();
}

void producer(void *aux UNUSED)
{
	while(1) {
		lock_acquire(&mutex);
			// if no consumer thread, eventually exit, numC == -1
			// if there in nothing to read, exit
			while (numC == -1 || i+1 >= len) {

				// few test for test serilizability among various
				// test cases run, as mutex output_lock expects them
				// to be 0. we used -1 for no corresponding thread.
				if (numC == -1) numC = 0;
				if (numP == -1) numP = 0;
				else --numP;	// producer will exit eventually, so its obselete.

				// broadcast to all producer and consumer before it exit
				// to let them informed that either no data will be 
				// produced or there would be no consumer to consume the
				// data. So don't block. This ensures that all producers
				// wake up and reach here to wind up. In other case, it
				// will wake up all consumer to read whatever available
				// and finish as there won't be producer anymore.
				cond_broadcast(&empty, &mutex);
				cond_broadcast(&full, &mutex);

				if (pl.holder == thread_current()) lock_release(&pl);
				lock_release(&mutex);
				thread_exit();
			}

			/* waiting if full and there are some consumers to read */
			while (k == SZ_BUFF && numC) cond_wait(&full, &mutex);	// waiting if full 

			if (i+1 < len) {	// if something to read
				// This lock we introduced just to introduce serializability
				// on i, so that producers have atomic access to string and
				// producer doesn't produce same thing so output will be like hello world.
				lock_acquire(&pl);
					buf[in++] = string[i++];
					in %= SZ_BUFF;
					++k;
				lock_release(&pl);
			}
			cond_signal(&empty, &mutex);
		lock_release(&mutex);
	}
}

void consumer(void *aux UNUSED)
{
	while(1) {
		lock_acquire(&mutex);
		// numP == -1 implies 0 producer "threads", so exit
		// consumer exit when there is noting to read form shared
		// buf and all producers have finished because there was
		// nothing to read from string[]

			while ((!k && !numP && i+1 >= len) || numP == -1) {
				// few test for test serilizability among various
				// test cases run
				if (numP == -1) numP = 0;	
				if (numC == -1) numC = 0;
				else --numC;

				// broadcast to producers that there won't be any consumer
				// to consume buffer, hence no need to block for it
				if (numC == 0) cond_broadcast(&full, &mutex);
				if (pl.holder == thread_current()) lock_release(&pl);

				lock_release(&mutex);
				thread_exit();
			}

			// if there are some producer, I am sure they will produce
			// waiting for a while if buffer is empty (k == 0)
			while (k == 0 && numP) cond_wait(&empty, &mutex);	// waiting if empty

	//		lock_acquire(&pl);
			if (k > 0) {
				--k;
				// print whatever I consume and increase my out index
				// in a circular fashion and decrease the buf char count k
				printf("%c\n",buf[out]);
				out = (out+1) % SZ_BUFF;
			}
	//		lock_release(&pl);
			cond_signal(&full, &mutex);

		lock_release(&mutex);
	}
}

void producer_consumer(unsigned int num_producer, unsigned int num_consumer)
{
	lock_acquire(&output_lock);
	{
		unsigned int np, nc;

		len = sizeof(string);

	//	printf("np = %d, nc = %d, len = %d %s holding\n", num_producer, num_consumer, len, output_lock.holder->name);
		cond_init(&empty);
		cond_init(&full);
		lock_init(&mutex);
		lock_init(&pl);
		i = in = out = k = 0;

		numP = num_producer;
		numC = num_consumer;

		// Since we wrote this program before email discussion on
		// output behaviour, this simply marks producers consumers
		// -1 if there won't be any corresponding thread. It ensures 
		// that all test cases are run and no blocking happens.

		if (!(numP == 0 && numC == 0)) {
			if (num_producer == 0) numP = -1;//num_producer;
			if (num_consumer == 0) numC = -1;
		}

		for (nc = 0; nc < num_consumer; ++nc) {
			char name[16];
			snprintf(name, sizeof name, "consumer %u", nc);
			thread_create(name, PRI_DEFAULT, consumer, 0);
		}
		for (np = 0; np < num_producer; ++np) {
			char name[16];
			snprintf(name, sizeof name, "producer %u", np);
			thread_create(name, PRI_DEFAULT, producer, 0);
		}
	}

	// yield the test thread and p/c would run
	// could also use timer_sleep
	
	while(1) 
	{
//		timer_sleep(5000);

		// if no producer consumer, run next test
		if (numP == 0 && numC == 0) break;
		thread_yield();
	}

	printf("\n");
	lock_release(&output_lock);
}
